__all__ = ['parse_command_line', 'log_config', 'init', 'schedule_tasks', 'clean']


import argparse
import datetime
import hmac
import logging
import os
import re
import sys
import types

import util


__author__  = 'ZL <837806295@qq.com>'
__product__ = 'checkit'
__version__ = '2.4.5'
__date__    = '2023/04/08'


log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())  # 如果宿主未配置日志处理器，则丢弃日志

args = types.SimpleNamespace()

password = None

digest_key = None
digest_length = 64
digest_extension = '.info'


class Digest:
    """封装HMAC-SHA256算法

    HMAC-SHA256算法是带密钥的摘要算法，对密钥长度没有要求，对数据长度也没有要求，生成32字节
    的摘要。封装后会将生成的32字节摘要用十六进制字符表示，所以最后的结果是64个十六进制的字符
    """

    def __init__(self, key):
        if not isinstance(key, bytes) or not key:
            log.critical('Digest | init ... failed ... exit | illegal input')
            sys.exit(1)

        try:
            self.generator = hmac.new(key, digestmod='sha256')
        except Exception:
            log.exception('Digest | init ... exception ... exit')
            sys.exit(1)

    def append_data(self, data):
        if not isinstance(data, bytes) or not data:
            log.warning('Digest | append data ... failed | illegal input')
            return

        try:
            self.generator.update(data)
        except Exception:
            log.exception('Digest | append data ... exception ... exit')
            sys.exit(1)

    def digest(self):
        try:
            mac = self.generator.hexdigest()
        except Exception:
            log.exception('Digest | digest ... exception ... exit')
            sys.exit(1)

        return mac


def is_supported_key(key):
    """是否是可支持的密钥字符

    HMAC-SHA256算法对密钥是没有限制的，不过为了方便输入密钥，这里只允许可打印字符作为密钥字
    符，一共是95个字符：0-9a-zA-Z `~!@#$%^&*()-_=+[{}]\|;:'",<.>/?
    """

    if not isinstance(key, str) or not key:
        log.error('is supported key ... no | illegal input')
        return False

    if not util.is_printable_ascii(key):
        log.error('is supported key ... no')
        return False

    log.debug('is support key ... yes')
    return True

def get_digest_key(key_sha512):
    if (key_sha512 is not None
            and not isinstance(key_sha512, str)
            and not isinstance(key_sha512, bool)):
        log.critical('get digest key ... failed ... exit | illegal input')
        sys.exit(1)

    if key_sha512 is None:
        log.warning('get digest key ... ignored | default')
        return '0123456789abcdef'

    if not key_sha512:
        log.info('get digest key ... ongoing | no check')
        return util.input_double('digest password', 3, is_supported_key)
    else:
        log.info('get digest key ... ongoing | check')
        return util.input_with_sha512('digest password', 3, is_supported_key,
                                      key_sha512)

def generate_digest_path(path):
    """生成摘要存储路径

    为指定的文件生成其摘要的存储路径，这里生成方法就是在原文件路径上附加一个扩展名
    """

    if not isinstance(path, str) or not path:
        log.error('generate digest path ... failed | illegal input')
        return ''

    if util.has_extension(path, digest_extension):
        log.debug(
            'generate digest path ... ongoing | path: %s, extension: %s',
            path, digest_extension)

    log.debug('generate digest path ... successed')
    return path + digest_extension

def is_digest_file(path):
    """不查看文件内容，筛查是不是存储摘要的文件

    扩展名符合要求，文件大小符合要求，剥离扩展名后路径是非空文件，则认为是存储摘要的文件
    """

    if not isinstance(path, str) or not path:
        log.error('is digest file ... no | illegal input')
        return False

    if not os.path.isfile(path):
        log.debug('is digest file .... no | not file: %s', path)
        return False

    _, filename = os.path.split(path)
    if (not util.has_extension(path, digest_extension)
            or filename == digest_extension):
        log.debug('is digest file .... no | extension')
        return False

    if os.path.getsize(path) != digest_length:
        log.debug('is digest file ... no | file size')
        return False

    strip_extension_path = path[: len(path) - len(digest_extension)]
    if (not os.path.isfile(strip_extension_path)
            or os.path.getsize(strip_extension_path) <= 0):
        log.debug('is digest file ... no | strip extension path')
        return False

    log.debug('is digest file ... yes')
    return True

def get_digest(path):
    """获取文件已创建的摘要
    """

    if not isinstance(path, str) or not path or not os.path.isfile(path):
        log.warning('get digest ... failed | illegal input')
        return ''

    digest_path = generate_digest_path(path)
    if not is_digest_file(digest_path):
        log.warning('get digest ... failed | no digest')
        return ''

    try:
        with open(digest_path, 'rb') as f:
            mac = f.read()
    except Exception:
        log.exception('get digest ... exception')
        return ''

    if not util.is_hex_ascii2(mac):
        log.warning('get digest ... failed | illegal digest')
        return ''

    log.debug('get digest ... successed')
    return mac.decode('utf-8')

def choose_requested_files(root, files, ignore_hide):
    """选择需要被处理的文件
    """

    if not isinstance(root, str) or not root:
        log.error('choose requested files ... failed | illegal input')
        return []

    if not isinstance(files, list) or not files:
        log.warning('choose requested files ... ignored | invalid input')
        return []

    requested = []
    for filename in files:
        path = os.path.join(root, filename)
        if util.is_hide(filename) and ignore_hide:
            log.info('choose requested files ... ongoing | ignore: %s', path)
            continue

        if is_digest_file(path):
            log.debug('choose requested files ... ongoing | not hit: %s', path)
            continue

        requested.append(path)
        log.debug('choose requested files ... ongoing | hit: %s', path)

    log.debug('choose requested files ... successed')
    return requested

def choose_special_file(root, files, ignore_hide):
    """选取具有摘要扩展名但不是摘要文件的特殊文件
    """

    if not isinstance(root, str) or not root:
        log.error('choose special file ... failed | illegal input')
        return []

    if not isinstance(files, list) or not files:
        log.warning('choose special file ... ignored | invalid input')
        return []

    requested = []
    for filename in files:
        path = os.path.join(root, filename)
        if util.is_hide(filename) and ignore_hide:
            log.info('choose special file ... ongoing | ignore: %s', path)
            continue

        if (util.has_extension(filename, digest_extension)
                and not is_digest_file(path)):
            requested.append(path)
            log.debug('choose special file ... ongoing | hit: %s', path)
            continue

        log.debug('choose special file ... ongoing | not hit: %s', path)

    log.debug('choose special file ... successed')
    return requested

def count_special_file(root, files, ignore_hide):
    """统计具有摘要扩展名但不是摘要文件的特殊文件数量
    """

    if not isinstance(root, str) or not root:
        log.error('count special file ... failed | illegal input')
        return 0

    if not isinstance(files, list) or not files:
        log.warning('count special file ... ignored | invalid input')
        return 0

    requested = 0
    for filename in files:
        path = os.path.join(root, filename)
        if util.is_hide(filename) and ignore_hide:
            log.info('count special file ... ongoing | ignore: %s', path)
            continue

        if (util.has_extension(filename, digest_extension)
                and not is_digest_file(path)):
            requested += 1
            log.debug('count special file ... ongoing | hit: %s', path)
            continue

        log.debug('count special file ... ongoing | not hit: %s', path)

    log.debug('count special file ... successed')
    return requested

def create_digest_for_file(path, key, bytes_per_read, force):
    """使用HMAC-SHA256算法为单个文件创建摘要

    会在原文件所在目录生成一个存储摘要的文件，摘要文件名由原文件完整文件名附加一个扩展名构成，
    这个扩展名默认是“.info”

    Args:
        path(str): 待生成摘要的文件的绝对路径
        key(bytes): HMAC-SHA256算法所需密钥
        bytes_per_read(int): 以字节为单位指定每次读文件大小
        force(bool): 当用来存储摘要的文件已存在时，是替换，还是跳过

    Returns:
        成功返回True，跳过返回None，失败返回False

        有三种情况会跳过，一种是空文件时，一种是摘要文件时，一种是存储摘要的文件已经存在且决
        定不替换时

    Return type: bool or None

    Raises:
        SystemExit
    """

    if (not is_supported_key(key)
            or not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not isinstance(bytes_per_read, int)):
        log.error('create digest for file ... failed | illegal input')
        return False

    if os.path.getsize(path) <= 0:
        log.warning('create digest for file ... ignored | empty file: %s', path)
        return None

    if is_digest_file(path):
        log.warning('create digest for file ... ignored | digest file: %s',
                    path)
        return None

    digest_path = generate_digest_path(path)
    if os.path.exists(digest_path):
        if force:
            if not os.path.isfile(digest_path):
                log.error('create digest for file ... failed | not file: %s',
                          digest_path)
                return False
        else:
            log.warning('create digest for file ... ignored | exist: %s',
                        digest_path)
            return None

    try:
        with open(path, 'rb') as f:
            d = Digest(key.encode('utf-8'))

            while True:
                buffer = f.read(bytes_per_read)

                if buffer:
                    d.append_data(buffer)
                else:
                    break

            mac = d.digest()
    except Exception:
        log.exception('create digest for file ... exception')
        return False

    if not isinstance(mac, str) or len(mac) != digest_length:
        log.error('create digest for file ... failed | mac')
        return False

    try:
        with open(digest_path, 'wb') as f:
            f.write(mac.encode('utf-8'))
    except Exception:
        log.exception('create digest for file ... exception')
        return False

    log.info('create digest for file ... successed')
    return True

def create_for_file():
    log.info('create for file ... ongoing')

    tristate = create_digest_for_file(args.path,
                                      digest_key,
                                      args.mb_per_read * 1024 * 1024,
                                      args.force)
    if tristate is None:
        util.prompt('Ignore')
    else:
        util.prompt_assert(tristate, 'Create digest')

    log.info('create for file ... done')

def create_digest_for_file_in_sandbox(path, key, bytes_per_read, force):
    """对单个文件生成摘要

    对单个文件生成摘要，会拦截导致的任何异常，防止直接退出，以免影响别的任务
    """

    log.info('create digest for file in sandbox ... ongoing | path: %s, '
             'bytes_per_read: %d, force: %s',
             path,
             bytes_per_read,
             force)

    try:
        tristate = create_digest_for_file(path, key, bytes_per_read, force)
    except Exception:
        log.exception('create digest for file in sandbox ... exception | '
                      'exception')
        return False
    except SystemExit as e:
        log.exception(
            'create digest for file in sandbox ... exception | systemexit, '
            'exit code: %d',
            e.code)
        return False

    log.info('create digest for file in sandbox ... done')
    return tristate

def create_for_dir():
    log.info('create for dir ... ongoing')

    def on_error(e):
        log.critical('create for dir ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = [0]
    ignored = []
    failed = []
    for root, dirs, files in os.walk(args.path, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        # 选择需要的文件，然后为其生成摘要，统计执行结果
        requested = choose_requested_files(root, files, not args.expose)
        for path in requested:
            util.prompt_status('Create digest for %s' % path, 'ongoing')
            tristate = create_digest_for_file_in_sandbox(
                path,
                digest_key,
                args.mb_per_read * 1024 * 1024,
                args.force)
            util.prompt_tristate(tristate, 'Create digest for %s' % path, False)
            util.make_statistics(path, tristate, total, ignored, failed)

    util.prompt_batch(total[0], ignored, failed)

    log.info('create for dir ... done')

def create():
    log.info('create ... ongoing')

    if os.path.isfile(args.path):
        create_for_file()
    elif os.path.isdir(args.path):
        create_for_dir()

    log.info('create ... done')

def remove_digest_for_file(path):
    """移除指定文件的摘要
    """

    if not isinstance(path, str) or not path or not os.path.isfile(path):
        log.error('remove digest for file ... failed | illegal input')
        return False

    digest_path = generate_digest_path(path)
    if not os.path.exists(digest_path):
        log.warning('remove digest for file ... ignored | nonexistent')
        return None

    if not is_digest_file(digest_path):
        log.error('remove digest for file ... failed | illegal digest | '
                  'path: %s',
                  digest_path)
        return False

    try:
        os.remove(digest_path)
    except Exception:
        log.exception('remove digest for file ... exception')
        return False

    log.info('remove digest for file ... successed')
    return True

def remove_for_file():
    log.info('remove for file ... ongoing')

    tristate = remove_digest_for_file(args.path)
    if tristate is None:
        util.prompt('Ignore')
    else:
        util.prompt_assert(tristate, 'Remove digest')

    log.info('remove for file ... done')

def remove_digest_for_file_in_sandbox(path):
    """对单个文件移除摘要

    对单个文件移除摘要，会拦截导致的任何异常，防止直接退出，以免影响别的任务
    """

    log.info('remove digest for file in sandbox ... ongoing | path: %s', path)

    try:
        tristate = remove_digest_for_file(path)
    except Exception:
        log.exception('remove digest for file in sandbox ... exception | '
                      'exception')
        return False
    except SystemExit as e:
        log.exception(
            'remove digest for file in sandbox ... exception | systemexit, '
            'exit code: %d',
            e.code)
        return False

    log.info('remove digest for file in sandbox ... done')
    return tristate

def remove_by_extension():
    log.info('remove by extension ... ongoing')

    def on_error(e):
        log.critical('remove by extension ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = [0]
    ignored = []
    failed = []
    for root, dirs, files in os.walk(args.path, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        # 按扩展名选择文件，然后为其移除摘要，统计执行结果
        requested = util.choose_file_by_extension(root, files, not args.expose,
                                                  digest_extension)
        for path in requested:
            ok = util.remove_file_in_sandbox(path)
            util.prompt_assert(ok, 'Remove %s' % path, False)
            util.make_statistics(path, ok, total, ignored, failed)

    util.prompt_batch(total[0], ignored, failed)

    log.info('remove by extension ... done')

def remove_by_default():
    log.info('remove by default ... ongoing')

    def on_error(e):
        log.critical('remove by default ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = [0]
    ignored = []
    failed = []
    for root, dirs, files in os.walk(args.path, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        # 选择需要的文件，然后为其移除摘要，统计执行结果
        requested = choose_requested_files(root, files, not args.expose)
        for path in requested:
            tristate = remove_digest_for_file_in_sandbox(path)
            util.prompt_tristate(tristate, 'Remove digest for %s' % path, False)
            util.make_statistics(path, tristate, total, ignored, failed)

    util.prompt_batch(total[0], ignored, failed)

    log.info('remove by default ... done')

def remove_for_dir():
    log.info('remove for dir ... ongoing')

    if args.extension:
        remove_by_extension()
    else:
        remove_by_default()

    log.info('remove for dir ... done')

def remove():
    log.info('remove ... ongoing')

    if os.path.isfile(args.path):
        remove_for_file()
    elif os.path.isdir(args.path):
        remove_for_dir()

    log.info('remove ... done')

def list_digest_for_file(path):
    """列出指定文件是否已摘要
    """

    if not isinstance(path, str) or not path or not os.path.isfile(path):
        log.error('list digest for file ... error | illegal input')
        return False

    digest_path = generate_digest_path(path)
    if not os.path.exists(digest_path):
        log.info('list digest for file ... no | nonexistent')
        return False

    if not is_digest_file(digest_path):
        log.info('list digest for file ... no | illegal digest | '
                 'path: %s',
                 digest_path)
        return False

    log.info('list digest for file ... yes')
    return True

def list_for_file():
    log.info('list for file ... ongoing')

    ok = list_digest_for_file(args.path)
    if ok:
        util.prompt('Digest')
    else:
        util.prompt('No digest')

    log.info('list for file ... done')

def list_special():
    log.info('list special ... ongoing')

    def on_error(e):
        log.critical('list special ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = 0 if args.count else []
    for root, dirs, files in os.walk(args.path, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        if args.count:
            requested = count_special_file(root, files, not args.expose)
            total += requested
        else:
            requested = choose_special_file(root, files, not args.expose)
            total += requested

    util.prompt_batch2(total)

    log.info('list special ... done')

def list_empty():
    log.info('list empty ... ongoing')

    def on_error(e):
        log.critical('list empty ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = 0 if args.count else []
    for root, dirs, files in os.walk(args.path, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        if args.count:
            requested = util.count_empty_file(root, files, not args.expose)
            total += requested
        else:
            requested = util.choose_empty_file(root, files, not args.expose)
            total += requested

    util.prompt_batch2(total)

    log.info('list empty ... done')

def list_by_extension():
    log.info('list by extension ... ongoing')

    def on_error(e):
        log.critical('list by extension ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = 0 if args.count else []
    for root, dirs, files in os.walk(args.path, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        if args.count:
            requested = util.count_file_by_extension(root,
                                                     files,
                                                     not args.expose,
                                                     digest_extension)
            total += requested
        else:
            requested = util.choose_file_by_extension(root,
                                                      files,
                                                      not args.expose,
                                                      digest_extension)
            total += requested

    util.prompt_batch2(total)

    log.info('list by extension ... done')

def make_statistics(range, count, requested, total, digest, no_digest):
    """按要求做统计

    count为True时，digest和no_digest第一个元素是0
    """

    if (not isinstance(range, str)
            or not range
            or not isinstance(requested, list)
            or not isinstance(total, list)
            or not total
            or not isinstance(digest, list)
            or not isinstance(no_digest, list)
            or (count and (digest[0] != 0 or no_digest[0] != 0))):
        log.critical('make statistics ... failed ... exit | illegal input')
        sys.exit(1)

    if count:
        for path in requested:
            total[0] += 1

            digest_path = generate_digest_path(path)
            if is_digest_file(digest_path):
                digest[0] += 1
            else:
                no_digest[0] += 1

        return

    for path in requested:
        total[0] += 1

        digest_path = generate_digest_path(path)
        if is_digest_file(digest_path):
            if range == 'both' or range == 'digest':
                digest.append(path)
        else:
            if range == 'both' or range == 'no_digest':
                no_digest.append(path)

def prompt_batch(range, total, digest, no_digest):
    """输出批处理的规范提示信息
    """

    if (not isinstance(range, str)
            or not range
            or not isinstance(total, int)
            or not isinstance(digest, int)
            or not isinstance(no_digest, int)):
        log.critical('prompt batch ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt batch ... ongoing | total: %d, digest: %d, no_digest: %d',
             total,
             digest,
             no_digest)

    now = datetime.datetime.now()

    if range == 'both':
        prompt = ('[%s]Total %d, digest %d, no digest %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'),
                     total,
                     digest,
                     no_digest))
    elif range == 'digest':
        prompt = ('[%s]Total %d, digest %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'),
                     total,
                     digest))
    elif range == 'no_digest':
        prompt = ('[%s]Total %d, no digest %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'),
                     total,
                     no_digest))
    print(prompt)

def prompt_batch2(range, total, digest, no_digest):
    """输出批处理的规范提示信息
    """

    if (not isinstance(range, str)
            or not range
            or not isinstance(total, int)
            or not isinstance(digest, list)
            or not isinstance(no_digest, list)):
        log.critical('prompt batch ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt batch ... ongoing | total: %d, digest: %d, no_digest: %d',
             total,
             len(digest),
             len(no_digest))

    now = datetime.datetime.now()

    if range == 'both' and total > 0:
        prompt = '[%s]Report:' % now.strftime('%Y-%m-%d %H:%M:%S')
        print(prompt)
    elif range == 'digest' and digest:
        prompt = '[%s]Report:' % now.strftime('%Y-%m-%d %H:%M:%S')
        print(prompt)
    elif range == 'no_digest' and no_digest:
        prompt = '[%s]Report:' % now.strftime('%Y-%m-%d %H:%M:%S')
        print(prompt)

    if range == 'both' or range == 'digest':
        if digest:
            log.info('prompt batch ... ongoing | digest')
            print('Digest:')
        for info in digest:
            if isinstance(info, str):
                log.info('prompt batch ... ongoing | %s', info)
                print(info)

    if range == 'both' or range == 'no_digest':
        if no_digest:
            log.info('prompt batch ... ongoing | no digest')
            print('No digest:')
        for info in no_digest:
            if isinstance(info, str):
                log.info('prompt batch ... ongoing | %s', info)
                print(info)

    if range == 'both':
        prompt = ('[%s]Total %d, digest %d, no digest %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'),
                     total,
                     len(digest),
                     len(no_digest)))
    elif range == 'digest':
        prompt = ('[%s]Total %d, digest %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'),
                     total,
                     len(digest)))
    elif range == 'no_digest':
        prompt = ('[%s]Total %d, no digest %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'),
                     total,
                     len(no_digest)))
    print(prompt)

def list_by_default():
    log.info('list by default ... ongoing')

    def on_error(e):
        log.critical('list by default ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = [0]
    digest = [0] if args.count else []
    no_digest = [0] if args.count else []
    for root, dirs, files in os.walk(args.path, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        requested = choose_requested_files(root, files, not args.expose)
        make_statistics(args.range, args.count, requested, total, digest,
                        no_digest)

    if args.count:
        prompt_batch(args.range, total[0], digest[0], no_digest[0])
    else:
        prompt_batch2(args.range, total[0], digest, no_digest)

    log.info('list by default ... done')

def list_for_dir():
    log.info('list for dir ... ongoing')

    if args.range in ['digest', 'no_digest', 'both']:
        list_by_default()
    elif args.range == 'extension':
        list_by_extension()
    elif args.range == 'special':
        list_special()
    elif args.range == 'empty':
        list_empty()

    log.info('list for dir ... done')

def list_():
    log.info('list ... ongoing')

    if os.path.isfile(args.path):
        list_for_file()
    elif os.path.isdir(args.path):
        list_for_dir()

    log.info('list ... done')

def check_digest_for_file(path, key, bytes_per_read):
    """使用HMAC-SHA256算法为单个文件校验摘要

    Args:
        path(str): 待校验摘要的文件的绝对路径
        key(bytes): HMAC-SHA256算法所需密钥
        bytes_per_read(int): 以字节为单位指定每次读文件大小

    Returns:
        成功返回True，跳过返回None，失败返回False

        有两种情况会跳过，一种是空文件时，另一种是存储摘要的文件不存在时

    Return type: bool or None

    Raises:
        SystemExit
    """

    if (not is_supported_key(key)
            or not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not isinstance(bytes_per_read, int)):
        log.error('check digest for file ... failed | illegal input')
        return False

    if os.path.getsize(path) <= 0:
        log.warning('check digest for file ... ignored | empty file: %s', path)
        return None

    digest_path = generate_digest_path(path)
    if not os.path.exists(digest_path):
        log.warning('check digest for file ... ignored | nonexistent')
        return None
    else:
        if not os.path.isfile(digest_path):
            log.error('check digest for file ... failed | not file: %s',
                      digest_path)
            return False

    try:
        with open(path, 'rb') as f:
            d = Digest(key.encode('utf-8'))

            while True:
                buffer = f.read(bytes_per_read)

                if buffer:
                    d.append_data(buffer)
                else:
                    break

            mac = d.digest()
    except Exception:
        log.exception('check digest for file ... exception')
        return False

    if not isinstance(mac, str) or len(mac) != digest_length:
        log.error('check digest for file ... failed | mac')
        return False

    mac2 = get_digest(path)
    if mac.lower() != mac2.lower():
        log.error('check digest for file ... failed | not equal: %s != %s',
                  mac, mac2)
        return False

    log.info('check digest for file ... successed')
    return True

def check_for_file():
    log.info('check for file ... ongoing')

    tristate = check_digest_for_file(args.path,
                                     digest_key,
                                     args.mb_per_read * 1024 * 1024)
    if tristate is None:
        util.prompt('Ignore')
    else:
        util.prompt_assert(tristate, 'Check digest')

    log.info('check for file ... done')

def check_digest_for_file_in_sandbox(path, key, bytes_per_read):
    """对单个文件校验摘要

    对单个文件校验摘要，会拦截导致的任何异常，防止直接退出，以免影响别的任务
    """

    log.info('check digest for file in sandbox ... ongoing | path: %s, '
             'bytes_per_read: %d',
             path,
             bytes_per_read)

    try:
        tristate = check_digest_for_file(path, key, bytes_per_read)
    except Exception:
        log.exception('check digest for file in sandbox ... exception | '
                      'exception')
        return False
    except SystemExit as e:
        log.exception(
            'check digest for file in sandbox ... exception | systemexit, '
            'exit code: %d',
            e.code)
        return False

    log.info('check digest for file in sandbox ... done')
    return tristate

def check_for_dir():
    log.info('check for dir ... ongoing')

    def on_error(e):
        log.critical('check for dir ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = [0]
    ignored = []
    failed = []
    for root, dirs, files in os.walk(args.path, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        # 选择需要的文件，然后为其校验摘要，统计执行结果
        requested = choose_requested_files(root, files, not args.expose)
        for path in requested:
            util.prompt_status('Check digest for %s' % path, 'ongoing')
            tristate = check_digest_for_file_in_sandbox(
                path,
                digest_key,
                args.mb_per_read * 1024 * 1024)
            util.prompt_tristate(tristate, 'Check digest for %s' % path, False)
            util.make_statistics(path, tristate, total, ignored, failed)

    util.prompt_batch(total[0], ignored, failed)

    log.info('check for dir ... done')

def check():
    log.info('check ... ongoing')

    if os.path.isfile(args.path):
        check_for_file()
    elif os.path.isdir(args.path):
        check_for_dir()

    log.info('check ... done')

def parse_command_line():
    parser = argparse.ArgumentParser(
        prog=__product__,
        description='管理指定文件或者指定目录下的文件的摘要信息，以防篡改欺骗')

    parser.add_argument('-v', '--version',
                        action='version',
                        version='%(prog)s ' + __version__)

    subparsers = parser.add_subparsers(
        title='subcommands',
        required=True,
        dest='subcommand',
        description='子命令集，由相应的子命令完成相应的功能')

    common = argparse.ArgumentParser(add_help=False)
    common.add_argument('-k', '--key',
                        type=util.arg_type_sha512,
                        nargs='?',
                        default=None,
                        const=False,
                        help='摘要算法需要密码，如果这个选项给出密码的sha512串，则会使'
                             '用其来校验密码输入是否正确，如果这个选项没有给值，则不校'
                             '验密码输入是否正确，只是输入两次做前后对比校验，不使用这个'
                             '选项时会使用默认密码“0123456789abcdef”')
    common.add_argument('-m', '--mb_per_read',
                        type=util.arg_type_mb_per_read,
                        default=4,
                        help='每次操作读文件数据的大小，单位是M，最小为1，最大为1024，'
                             '默认是4')

    # create子命令，用于生成摘要信息
    create = subparsers.add_parser(
        'create',
        help='生成摘要信息',
        parents=[util.arg_log(), util.arg_shutdown(), common])
    create.add_argument('path',
                        type=util.arg_type_path,
                        help='需要生成摘要信息的文件或者目录的完整路径')
    create.add_argument('-f', '--force',
                        action='store_true',
                        help='用来存储摘要信息的文件存在时是否覆盖，有此选项则覆盖，无此'
                             '选项则跳过')
    create.add_argument('-e', '--expose',
                        action='store_true',
                        help='为目录下文件生成摘要信息时，是否跳过以“.”字符开头的文件或'
                             '者目录，有此选项则不跳过，无此选项则跳过')

    # remove子命令，用于移除摘要信息
    remove = subparsers.add_parser(
        'remove',
        help='移除摘要信息',
        parents=[util.arg_log()])
    remove.add_argument('path',
                        type=util.arg_type_path,
                        help='需要移除摘要信息的文件或者目录的完整路径')
    remove.add_argument('-e', '--expose',
                        action='store_true',
                        help='为目录下文件移除摘要信息时，是否跳过以“.”字符开头的文件或'
                             '者目录，有此选项则不跳过，无此选项则跳过')
    remove.add_argument('-t', '--extension',
                        action='store_true',
                        help='为目录下文件移除摘要信息时，有此选项则移除所有以摘要信息文'
                             '件扩展名为扩展名的文件，无此选项则会根据多个条件检测结果相'
                             '对可靠的去移除')

    # list子命令，列出摘要状态
    list_ = subparsers.add_parser(
        'list',
        help='列出摘要状态',
        parents=[util.arg_log()])
    list_.add_argument('path',
                       type=util.arg_type_path,
                       help='需要列出摘要状态的文件或者目录的完整路径')
    list_.add_argument(
        '-r', '--range',
        choices=[
            'digest',
            'no_digest',
            'both',
            'extension',
            'special',
            'empty'
        ],
        default='no_digest',
        help='为目录下文件列出摘要状态时，控制列出范围，有digest、no_digest、both、'
             'extension、special和empty这些值可用，分别表示列出已摘要的、列出未摘要的、'
             '同时列出已摘要的和未摘要的、列出具有摘要扩展名的、列出具有摘要扩展名但不是摘'
             '要文件的和空文件，默认是no_digest，当count选项启用时，就只统计数量'
    )
    list_.add_argument('-e', '--expose',
                       action='store_true',
                       help='为目录下文件列出摘要状态时，是否跳过以“.”字符开头的文件或者'
                            '目录，有此选项则不跳过，无此选项则跳过')
    list_.add_argument('-c', '--count',
                       action='store_true',
                       help='为目录下文件列出摘要状态时，有此选项则只统计数量')

    # check子命令，校验摘要信息
    check = subparsers.add_parser(
        'check',
        help='校验摘要信息',
        parents=[util.arg_log(), util.arg_shutdown(), common])
    check.add_argument('path',
                       type=util.arg_type_path,
                       help='需要使用摘要信息进行校验的文件或者目录的完整路径')
    check.add_argument('-e', '--expose',
                        action='store_true',
                        help='为目录下文件校验摘要信息时，是否跳过以“.”字符开头的文件或'
                             '者目录，有此选项则不跳过，无此选项则跳过')

    global args
    args = parser.parse_args()

def log_config():
    if hasattr(args, 'log'):
        if hasattr(args, 'log_identifier') and args.log_identifier:
            return (
                '%s_v%s' % (__product__, __version__),
                args.log,
                args.log_identifier
            )
        else:
            return ('%s_v%s' % (__product__, __version__), args.log)

    return None

def init():
    log.info('program version: %s %s', __product__, __version__)
    log.debug('command line: %s', sys.argv)
    log.debug('parse args: %s', args)

    # 获取操作系统密码以便任务完成后自动关机
    if hasattr(args, 'shutdown'):
        global password
        password = util.get_os_password(args.shutdown)

    if hasattr(args, 'key'):
        global digest_key
        digest_key = get_digest_key(args.key)

def schedule_tasks():
    if args.subcommand == 'create':
        create()
    elif args.subcommand == 'remove':
        remove()
    elif args.subcommand == 'list':
        list_()
    elif args.subcommand == 'check':
        check()

def clean():
    # 自动关机
    if hasattr(args, 'shutdown'):
        util.shutdown(password)
